// Copyright (c) 2023 kevin Yang
//
// This file shows how to use a non-streaming model to decode files
// Please refer to
// https://k2-fsa.github.io/sherpa/onnx/pretrained_models/index.html
// to download non-streaming models
using CommandLine;
using CommandLine.Text;
using SherpaOnnx;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SpeechRecognitionCSharp;

/// <summary>
/// 语音识别控制台应用程序类
/// 用于使用非流式模型解码音频文件
/// </summary>
public class SpeechRecognitionConsoleApp
{
    // 从配置文件中读取模型路径，如果配置文件不存在或没有指定，则使用默认值
    static string MODELPATH = "D:/programs11/DevSoft/AIProduct/Model-SenceVoice-sherpaOnnx/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17";


    /// <summary>
    /// 应用程序入口点
    /// 处理命令行参数并启动语音识别过程
    /// </summary>
    /// <param name="args">命令行参数</param>
    static void Main(string[] args)
    {
        // 创建命令行解析器，禁用默认帮助输出
        var parser = new CommandLine.Parser(with => with.HelpWriter = null);

        // 定义默认命令行参数，用于演示和测试
        // 包含模型路径、词汇表路径和测试音频文件路径
        
        // 从config.ini文件中读取配置
        string currentDirectory = AppDomain.CurrentDomain.BaseDirectory;
        string configFilePath = Path.Combine(currentDirectory, "config.ini");
        string doFolderPath = "do"; // 默认的do文件夹名称
        
        // 检查配置文件是否存在
        if (File.Exists(configFilePath))
        {
            // 读取配置文件的每一行
            string[] lines = File.ReadAllLines(configFilePath);
            foreach (string line in lines)
            {
                // 忽略空行和注释行
                if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
                    continue;
                
                // 分割键值对
                string[] parts = line.Split('=');
                if (parts.Length >= 2)
                {
                    string key = parts[0].Trim();
                    string value = parts[1].Trim();
                    
                    // 处理DoFolder配置项
                    if (key.Equals("DoFolder", StringComparison.OrdinalIgnoreCase))
                    {
                        doFolderPath = value;
                    }

                    if (key.Equals("MODELPATH", StringComparison.OrdinalIgnoreCase))
                    {
                        MODELPATH = value;
                    }
                }
            }
        }
        
        // 构建完整的do文件夹路径
        doFolderPath = Path.Combine(currentDirectory, doFolderPath);
        string targetFile = "D:/session.mp4"; // 默认文件路径
        
        // 检查do文件夹是否存在
        if (Directory.Exists(doFolderPath))
        {
            // 定义支持的音频和视频文件扩展名
            string[] supportedExtensions = new string[] { ".mp3", ".wav", ".flac", ".mp4", ".avi", ".mkv", ".mov" };
            
            // 获取do文件夹中的文件
            string[] files = Directory.GetFiles(doFolderPath);

            if (files.Length == 0 )
            {
                Console.WriteLine($"没有找到合适的文件，请检查文件夹{doFolderPath}");
                return;
            }

            // 查找第一个支持的音频或视频文件
            foreach (string file in files)
            {
                string extension = Path.GetExtension(file).ToLower();
                if (supportedExtensions.Contains(extension))
                {
                    targetFile = file;
                    // 将路径中的反斜杠替换为正斜杠
                    targetFile = targetFile.Replace('\\', '/');
                    break;
                }
            }
        }
 
        string str = $@"
            --sense-voice-model={MODELPATH}/model.int8.onnx \            
            --tokens={MODELPATH}/tokens.txt \            
            --files={targetFile} ";

       
        // 如果没有提供命令行参数，使用默认参数
        if (args.Length <= 0)
        {

            // 处理默认参数字符串，拆分并修剪空格
            // 如果没有提供命令行参数，使用默认参数
            if (args.Length <= 0)
            {

                // 处理默认参数字符串，拆分并修剪空格
                var xstr = str.Split('\\');
                List<string> list = new List<string>();
                foreach (var x in xstr)
                {
                    list.Add(x.Trim());
                }
                args = list.ToArray();
            }

        }

        // 解析命令行参数
        var parserResult = parser.ParseArguments<Options>(args);

        // 添加音频文件分割功能和视频转音频功能
        parserResult.WithParsed<Options>(options =>
        {
            // 检查并分割超过20分钟的音频文件，以及将视频文件转换为音频
            var processedFiles = new List<string>();
            var tempFiles = new List<string>(); // 用于存储临时转换的文件，后续清理

            foreach (var file in options.Files)
            {
                // 检查文件扩展名，处理视频文件
                string extension = Path.GetExtension(file).ToLower();
                string audioFilePath = file;

                // 如果是视频文件，先转换为mp3  .
                if (extension == ".mp4" || extension == ".avi" || extension == ".mkv" || extension == ".mov")
                {
                    // 不要拿很长的视频文件测试。等待时间你会怀疑 every thing ...
                    Console.WriteLine($"检测到视频文件: {file}，正在转换为音频...");
                    string tempDir = Path.Combine(Path.GetDirectoryName(file) ?? ".", "audioTmp");
                    Directory.CreateDirectory(tempDir);
                    string tempAudioFilePath = Path.Combine(tempDir, $"{Path.GetFileNameWithoutExtension(file)}_converted.mp3");

                    try
                    {
                        // 使用FFMpegCore将视频转换为mp3
                        FFMpegCore.FFMpegArguments
                            .FromFileInput(file)
                            .OutputToFile(tempAudioFilePath, true, options => options
                                .WithAudioBitrate(FFMpegCore.Enums.AudioQuality.Normal) // 128kbps
                                .WithAudioCodec("libmp3lame")
                                .WithAudioSamplingRate(16000) // 采样率16000Hz
                                .WithCustomArgument("-ac 1") // 单声道
                                .WithCustomArgument("-sample_fmt s16") // 位深度16位
                            )
                            .ProcessSynchronously();

                        audioFilePath = tempAudioFilePath;
                        tempFiles.Add(tempAudioFilePath);
                        //processedFiles.Add(tempAudioFilePath);
                        Console.WriteLine($"视频文件转换成功，生成音频文件: {audioFilePath}");
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"视频转换失败: {ex.Message}，将尝试直接处理原文件");
                    }
                }

                // 检查音频文件长度并分割
                try
                {   // 即使是分割成多个文件，也非常吃内存和硬盘。不要超过40min的音频。
                    double duration = AudioReader.GetAudioDuration(audioFilePath);
                    if (duration > 1200) // 20分钟 = 1200秒
                    {
                        Console.WriteLine($"文件 {audioFilePath} 长度为 {duration:F2} 秒，超过20分钟，正在分割...");
                        var splitFiles = AudioReader.SplitAudioFile(audioFilePath);
                        processedFiles.AddRange(splitFiles);
                        
                    }
                    else
                    {
                        processedFiles.Add(audioFilePath);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"获取音频时长失败: {ex.Message}");
                    processedFiles.Add(audioFilePath);
                }
            }

            // 直接修改原Options对象的Files属性
            options.Files = processedFiles;

            // 注册进程退出事件，用于清理临时文件
            AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
            {
                // 程序退出时清理临时文件
                foreach (var tempFile in tempFiles)
                {
                    try
                    {
                        if (File.Exists(tempFile))
                        {
                            File.Delete(tempFile);
                        }
                    }
                    catch { }
                }
            };
        });

        // 处理解析结果
        // 如果解析成功，调用Run方法
        // 如果解析失败，显示帮助信息
        parserResult
          .WithParsed<Options>(options => Run(options))
          .WithNotParsed(errs => DisplayHelp(parserResult, errs));
    }



    /// <summary>
    /// 显示命令行帮助信息
    /// 当命令行参数解析失败时调用
    /// </summary>
    /// <typeparam name="T">解析结果的类型</typeparam>
    /// <param name="result">解析结果对象</param>
    /// <param name="errs">解析错误集合</param>
    private static void DisplayHelp<T>(ParserResult<T> result, IEnumerable<Error> errs)
    {
        var usage = "";
        // 自动构建帮助文本
        var helpText = HelpText.AutoBuild(result, h =>
        {
            h.AdditionalNewLineAfterOption = false;
            h.Heading = usage;
            h.Copyright = "Copyright (c) 2023 kevin Yang";
            return HelpText.DefaultParsingErrorsHandler(result, h);
        }, e => e);


        // 输出帮助文本到控制台
        Console.WriteLine(helpText);
    }

    private static OfflineRecognizerConfig? GetRecognizerConfig(Options options)
    {

        OfflineRecognizerConfig config = new OfflineRecognizerConfig();
        // 配置特征提取参数
        config.FeatConfig.SampleRate = options.SampleRate;
        config.FeatConfig.FeatureDim = options.FeatureDim;

        // 配置模型参数
        config.ModelConfig.Tokens = options.Tokens;
        // 根据提供的参数确定模型类型并配置
        if (!string.IsNullOrEmpty(options.Encoder))
        {
            // 这是Transducer模型
            // Transducer模型由编码器、解码器和连接器组成
            config.ModelConfig.Transducer.Encoder = options.Encoder;
            config.ModelConfig.Transducer.Decoder = options.Decoder;
            config.ModelConfig.Transducer.Joiner = options.Joiner;
        }
        else if (!string.IsNullOrEmpty(options.Paraformer))
        {
            // 这是Paraformer模型
            config.ModelConfig.Paraformer.Model = options.Paraformer;
        }
        else if (!string.IsNullOrEmpty(options.NeMoCtc))
        {
            // 这是NeMo CTC模型
            config.ModelConfig.NeMoCtc.Model = options.NeMoCtc;
        }
        else if (!string.IsNullOrEmpty(options.DolphinModel))
        {
            // 这是Dolphin模型
            config.ModelConfig.Dolphin.Model = options.DolphinModel;
        }
        else if (!string.IsNullOrEmpty(options.ZipformerCtc))
        {
            // 这是Zipformer CTC模型
            config.ModelConfig.ZipformerCtc.Model = options.ZipformerCtc;
        }
        else if (!string.IsNullOrEmpty(options.TeleSpeechCtc))
        {
            // 这是TeleSpeech CTC模型
            config.ModelConfig.TeleSpeechCtc = options.TeleSpeechCtc;
        }
        else if (!string.IsNullOrEmpty(options.WhisperEncoder))
        {
            config.ModelConfig.Whisper.Encoder = options.WhisperEncoder;
            config.ModelConfig.Whisper.Decoder = options.WhisperDecoder;
            config.ModelConfig.Whisper.Language = options.WhisperLanguage;
            config.ModelConfig.Whisper.Task = options.WhisperTask;
        }
        else if (!string.IsNullOrEmpty(options.TdnnModel))
        {
            config.ModelConfig.Tdnn.Model = options.TdnnModel;
        }
        else if (!string.IsNullOrEmpty(options.SenseVoiceModel))
        {
            config.ModelConfig.SenseVoice.Model = options.SenseVoiceModel;
            config.ModelConfig.SenseVoice.UseInverseTextNormalization = options.SenseVoiceUseItn;
        }
        else if (!string.IsNullOrEmpty(options.MoonshinePreprocessor))
        {
            config.ModelConfig.Moonshine.Preprocessor = options.MoonshinePreprocessor;
            config.ModelConfig.Moonshine.Encoder = options.MoonshineEncoder;
            config.ModelConfig.Moonshine.UncachedDecoder = options.MoonshineUncachedDecoder;
            config.ModelConfig.Moonshine.CachedDecoder = options.MoonshineCachedDecoder;
        }
        else if (!string.IsNullOrEmpty(options.FireRedAsrEncoder))
        {
            config.ModelConfig.FireRedAsr.Encoder = options.FireRedAsrEncoder;
            config.ModelConfig.FireRedAsr.Decoder = options.FireRedAsrDecoder;
        }
        else
        {
            Console.WriteLine("Please provide a model");
            return null;
        }

        config.ModelConfig.ModelType = options.ModelType;
        config.DecodingMethod = options.DecodingMethod;
        config.MaxActivePaths = options.MaxActivePaths;
        config.HotwordsFile = options.HotwordsFile;
        config.HotwordsScore = options.HotwordsScore;
        config.RuleFsts = options.RuleFsts;

        config.ModelConfig.Debug = 0;
        return config;
    }


    /// <summary>
    /// 运行语音识别
    /// 配置识别器并处理音频文件
    /// </summary>
    /// <param name="options">命令行参数选项</param>
    private static void Run(Options options)
    {
        // 创建离线识别器配置
        OfflineRecognizerConfig? config = GetRecognizerConfig(options);
        if (config == null)
        {
            return;
        }

        var recognizer = new OfflineRecognizer((OfflineRecognizerConfig)config);

        var files = options.Files.ToArray();
        const int batchSize = 2; // 一次处理的文件数量，根据内存情况调整

        // 分批处理文件以减少内存使用
        for (int batchStart = 0; batchStart < files.Length; batchStart += batchSize)
        {
            int currentBatchSize = Math.Min(batchSize, files.Length - batchStart);
            Console.WriteLine($"处理批次 {batchStart / batchSize + 1}/{(files.Length + batchSize - 1) / batchSize}，文件数量: {currentBatchSize}");

            // 为当前批次创建流
            var streams = new List<OfflineStream>();
            var streamFileMap = new Dictionary<OfflineStream, string>(); // Map stream to file path
            var streamChunkMap = new Dictionary<OfflineStream, int>(); // Map stream to chunk index

            for (int i = 0; i < currentBatchSize; ++i)
            {
                string filePath = files[batchStart + i];
                AudioReader audioReader = new AudioReader(filePath);
                float[] samples = audioReader.Samples;
                int sampleRate = audioReader.SampleRate;

                // 进一步减小文件块大小，减少每个流的内存占用
                const int maxSamplesPerChunk = 500000; // 每个块的最大样本数，可根据内存调整
                if (samples.Length < maxSamplesPerChunk || options.NumThreads <= 1)
                {
                    // 小文件直接处理
                    var s = recognizer.CreateStream();
                    s.AcceptWaveform(sampleRate, samples);
                    streams.Add(s);
                    streamFileMap[s] = filePath;
                    streamChunkMap[s] = -1; // -1 indicates whole file
                }
                else
                {
                    // 大文件分成更多小块
                    int numChunks = (samples.Length + maxSamplesPerChunk - 1) / maxSamplesPerChunk;
                    for (int chunkIndex = 0; chunkIndex < numChunks; ++chunkIndex)
                    {
                        int startIndex = chunkIndex * maxSamplesPerChunk;
                        int actualChunkSize = Math.Min(maxSamplesPerChunk, samples.Length - startIndex);

                        float[] chunkSamples = audioReader.ReadSamples(startIndex, actualChunkSize);
                        var s = recognizer.CreateStream();
                        s.AcceptWaveform(sampleRate, chunkSamples);
                        streams.Add(s);
                        streamFileMap[s] = filePath;
                        streamChunkMap[s] = chunkIndex;
                    }
                }

                // 释放audioReader资源
                audioReader = null;
                samples = null;
                GC.Collect(); // 触发垃圾回收释放内存
            }

            // 处理当前批次的流
            Console.WriteLine($"--------处理当前批次的 {streams.Count} 个流--------------------");
            recognizer.Decode(streams);
            Console.WriteLine("-----当前批次解码完成-------------------");

            // Group streams by file path for result merging
            var groupedResults = new Dictionary<string, List<(OfflineStream Stream, int ChunkIndex)>>();
            // 记录开始时间
            DateTime startTime = DateTime.Now;

            foreach (var stream in streams)
            {
                string filePath = streamFileMap[stream];
                int chunkIndex = streamChunkMap[stream];

                if (!groupedResults.ContainsKey(filePath))
                {
                    groupedResults[filePath] = new List<(OfflineStream, int)>();
                }
                groupedResults[filePath].Add((stream, chunkIndex));
            }

            // 计算并显示耗时
            DateTime endTime = DateTime.Now;
            TimeSpan duration = endTime - startTime;
            Console.WriteLine($"流分组处理耗时: {duration.TotalMilliseconds:F2} 毫秒");

            // Sort chunks by index and merge results
            foreach (var fileGroup in groupedResults)
            {
                string filePath = fileGroup.Key;
                var sortedChunks = fileGroup.Value.OrderBy(chunk => chunk.ChunkIndex).ToList();

                // If there's only one chunk (or whole file), display as before
                if (sortedChunks.Count == 1 && sortedChunks[0].ChunkIndex == -1)
                {
                    var stream = sortedChunks[0].Stream;
                    var r = stream.Result;
                    DisplayResult(filePath, r);
                }
                else
                {
                    // For multi-chunk files, merge results
                    StringBuilder mergedText = new StringBuilder();
                    List<string> mergedTokens = new List<string>();
                    List<float> mergedTimestamps = new List<float>();
                    float lastTimestamp = 0;

                    foreach (var (stream, chunkIndex) in sortedChunks)
                    {
                        var r = stream.Result;
                        mergedText.Append(r.Text);

                        if (r.Tokens != null)
                        {
                            mergedTokens.AddRange(r.Tokens);
                        }

                        if (r.Timestamps != null && r.Timestamps.Length > 0)
                        {
                            // Adjust timestamps based on previous chunk's last timestamp
                            for (int j = 0; j < r.Timestamps.Length; j++)
                            {
                                mergedTimestamps.Add(r.Timestamps[j] + lastTimestamp);
                            }
                            lastTimestamp = mergedTimestamps[mergedTimestamps.Count - 1];
                        }
                    }

                    // For multi-chunk files, we can't create a complete OfflineRecognizerResult
                    // directly, so we'll create a fake result for display purposes
                    Console.WriteLine("--------------------");

                    //// 导出到lrc文件
                    //string lrcFilePath = Path.ChangeExtension(filePath, ".lrc");
                    //File.WriteAllText(lrcFilePath, newx.ResultWithTimespanLrc);
                    //Console.WriteLine($"已将带时间戳的结果导出到: {lrcFilePath}");
                    Console.WriteLine(filePath);
                    Console.WriteLine("Text: {0}", mergedText.ToString());
                    Console.WriteLine("Tokens: [{0}]", string.Join(", ", mergedTokens));
                    if (mergedTimestamps.Count > 0)
                    {
                        Console.Write("Timestamps: [");
                        var sep = string.Empty;
                        for (int k = 0; k != mergedTimestamps.Count; ++k)
                        {
                            Console.Write("{0}{1}", sep, mergedTimestamps[k].ToString("0.00"));
                            sep = ", ";
                        }
                        Console.WriteLine("]");

                        // Note: We can't create a real ParseResult with merged data
                        // since we don't have all the necessary information

                        Console.WriteLine("(Merged result - timestamps may not be fully accurate)");
                        Console.WriteLine(mergedText.ToString());

                    }
                    else
                    {
                        Console.WriteLine("--------------------");
                    }


                }
            }

            // 释放当前批次的所有资源
            streams.Clear();
            streamFileMap.Clear();
            streamChunkMap.Clear();
            GC.Collect();
            Console.WriteLine("当前批次资源已释放");
        }

        // 结束批次循环
        Console.WriteLine("所有文件处理完成");
    }

    /// <summary>
    /// 显示识别结果
    /// </summary>
    /// <param name="filePath">文件路径</param>
    /// <param name="result">识别结果</param>
    private static void DisplayResult(string filePath, SherpaOnnx.OfflineRecognizerResult result)
    {
        Console.WriteLine("--------------------");
        Console.WriteLine(filePath);
        Console.WriteLine("Text: {0}", result.Text);
        Console.WriteLine("Tokens: [{0}]", string.Join(", ", result.Tokens));
        if (result.Timestamps != null && result.Timestamps.Length > 0)
        {
            Console.Write("Timestamps: [");
            var sep = string.Empty;
            for (int k = 0; k != result.Timestamps.Length; ++k)
            {
                Console.Write("{0}{1}", sep, result.Timestamps[k].ToString("0.00"));
                sep = ", ";
            }
            Console.WriteLine("]");

            Console.WriteLine("------xxxxxxxxxxxxxxxx---------");
            var _parseResult = new ParseResult(result);
            Console.Write(_parseResult.ResultWithTimespanLrc);
            Console.WriteLine("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
            Console.Write(_parseResult.Result);
            Console.WriteLine("--------------------");

            // 导出到lrc文件
            string lrcFilePath = Path.ChangeExtension(filePath, ".lrc");
            File.WriteAllText(lrcFilePath, _parseResult.ResultWithTimespanLrc);
            _parseResult.ExportResult(lrcFilePath, fileExportType.LRC);
            string srtFilePath = Path.ChangeExtension(filePath, ".srt");
            _parseResult.ExportResult(srtFilePath, fileExportType.SRT);
            _parseResult.ExportResult(Path.ChangeExtension(filePath, ".txt"), fileExportType.TXT);

            Console.WriteLine($"已将带时间戳的结果导出到: {lrcFilePath}");


        }
        else
        {
            Console.WriteLine("--------------------");
        }
    }




}

